﻿namespace DCHealthCheck {

    #region Usings
    using Amib.Threading;
    using Contoso.ActiveDirectory;
    using DCHealthCheck.Common;
    using DCHealthCheck.DomainModel;
    using Microsoft.GroupPolicy;
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Text;
    using System.Text.RegularExpressions;
    using System.Threading;
    using System.Xml.Serialization; 
    #endregion

    /// <summary>
    /// Queries AD for domain controller computer accounts, sites, group polices, and sysvol folders that are used in testing
    /// </summary>
    public class StartupDataGenerator {

        #region Members
        /// <summary>
        /// Key: Site Name  Value: ADDataAccess.SiteProperties object
        /// </summary>
        public static readonly Dictionary<string, SiteProperties> ADSites;

        public static readonly List<ExcludedServer> ExcludedServers;

        public static readonly List<ComputerProperties> DCComputerAccounts;

        /// <summary>
        /// Key: Domain DNS name Value: GpoCollection
        /// </summary>
        public static readonly Dictionary<string, List<Gpo>> DomainGPOs;

        private static log4net.ILog Log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

        private static SmartThreadPool STP;
        #endregion

        #region Constructor
        static StartupDataGenerator() {

            ADSites = new Dictionary<string, SiteProperties>(StringComparer.OrdinalIgnoreCase);
            DCComputerAccounts = new List<ComputerProperties>();
            DomainGPOs = new Dictionary<string, List<Gpo>>(StringComparer.OrdinalIgnoreCase);
            ExcludedServers = new List<ExcludedServer>();
            GetExcludedServers();

            int threadPoolConcurrency = 5 * Environment.ProcessorCount;
            if (threadPoolConcurrency > 50) threadPoolConcurrency = 50;
            STPStartInfo stpStartInfo = new STPStartInfo() { IdleTimeout = 20 * 1000, MaxWorkerThreads = threadPoolConcurrency };
            stpStartInfo.PostExecuteWorkItemCallback = STPPostExecute;
            STP = new SmartThreadPool(stpStartInfo);
            STP.Concurrency = threadPoolConcurrency;

            Log.InfoFormat("[ThreadID: {0}] {1} ThreadPool Concurrency: {2}", Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), STP.Concurrency);

            ADDataAccess.Initialize();
        } 
        #endregion

        #region Methods
        public static Dictionary<string, SiteProperties> GetADSites(bool useCache = true) {
            Log.InfoFormat("[ThreadID: {0}] {1}", Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName());

            var startTime = DateTime.UtcNow;
            var adSites = new Dictionary<string, SiteProperties>(StringComparer.OrdinalIgnoreCase);

            try {
                if (useCache && ADSites.Count > 0) {
                    adSites = ADSites;
                }
                else {
                    var adSiteList = ADDataAccess.GetADObjectProperties<SiteProperties>(
                        ADDataAccess.ForestRootConfigurationNamingContext, SiteProperties.AttributesToGet, null, PartitionScope.LocalPartition);

                    for (int siteIndex = 0; siteIndex < adSiteList.Count; siteIndex++) {
                        if (!adSites.ContainsKey(adSiteList[siteIndex].Name)) {
                            adSites.Add(adSiteList[siteIndex].Name, adSiteList[siteIndex]);
                        }
                    }

                    Log.InfoFormat("[ThreadID: {0}] {1} Getting Site Servers.  Site count: {2}",
                        Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), adSites.Count);

                    foreach (var adSite in adSites.Values) {
                        STP.QueueWorkItem(adSite.GetSiteServers);
                    }

                    if (!STP.WaitForIdle(new TimeSpan(1, 0, 0))) {
                        throw new ApplicationException(string.Format("Timeout waiting for threadpool threads to complete.  Active threads: {0}  Waiting callbacks: {1}", STP.ActiveThreads, STP.WaitingCallbacks));
                    }

                    Log.InfoFormat("[ThreadID: {0}] {1} Getting NTDSSettings.", Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName());

                    foreach (var adSite in adSites.Values) {
                        foreach (var siteServer in adSite.SiteServers) {
                            STP.QueueWorkItem(siteServer.GetNTDSSettings);
                        }
                    }

                    if (!STP.WaitForIdle(new TimeSpan(1, 0, 0))) {
                        throw new ApplicationException(string.Format("Timeout waiting for threadpool threads to complete.  Active threads: {0}  Waiting callbacks: {1}", STP.ActiveThreads, STP.WaitingCallbacks));
                    }

                    Log.InfoFormat("[ThreadID: {0}] {1} Getting NTDSConnections.", Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName());

                    foreach (var adSite in adSites.Values) {
                        foreach (var siteServer in adSite.SiteServers) {
                            if (siteServer.NTDSSettings != null) {
                                STP.QueueWorkItem(siteServer.NTDSSettings.GetNTDSConnections);
                            }
                        }
                    }

                    if (!STP.WaitForIdle(new TimeSpan(1, 0, 0))) {
                        throw new ApplicationException(string.Format("Timeout waiting for threadpool threads to complete.  Active threads: {0}  Waiting callbacks: {1}", STP.ActiveThreads, STP.WaitingCallbacks));
                    }

                    foreach (var site in adSites.Values) {
                        try {
                            Log.InfoFormat(site.ToFormattedString());
                            Log.InfoFormat("Inter-site Topology Generator (ISTG): {0}", site.NTDSSiteSettings.InterSiteTopologyGenerator);

                            // initialize the SiteServers and NTDSConnection objects
                            foreach (var siteServer in site.SiteServers) {
                                try {
                                    // a server may not have a Settings object if it is being/has been decommissioned.
                                    if (siteServer.NTDSSettings == null) {
                                        Log.WarnFormat("  Server: {0} does not have an NTDSSettings object", siteServer.DnsHostName);
                                        continue;
                                    }

                                    Log.InfoFormat(siteServer.ToFormattedString());
                                    Log.InfoFormat("Connections:");
                                    foreach (var ntdsConnection in siteServer.NTDSSettings.NTDSConnections) {
                                        try {
                                            Log.InfoFormat(ntdsConnection.ToFormattedString());
                                        }
                                        catch (Exception e) {
                                            Log.WarnFormat("[ThreadID: {0}] {1} {2}", Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), e.VerboseExceptionString());
                                        }
                                    } // for (int serverConnectionIndex = 0; serverConnectionIndex < siteServer.NTDSSettings.NTDSConnections.Count; serverConnectionIndex++)
                                }
                                catch (Exception e) {
                                    Log.WarnFormat("[ThreadID: {0}] {1} {2}", Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), e.VerboseExceptionString());
                                }
                            } // for (int siteServerIndex = 0; siteServerIndex < adSite.SiteServers.Count; siteServerIndex++)
                        }
                        catch (Exception e) {
                            Log.WarnFormat("[ThreadID: {0}] {1} {2}", Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), e.VerboseExceptionString());
                        }
                    } // for (int siteIndex = 0; siteIndex < adSites.Count; siteIndex++)

                    if (adSites.Count > 0) {
                        lock (ADSites) {
                            ADSites.Clear();
                            foreach (var kvp in adSites) {
                                ADSites.Add(kvp.Key, kvp.Value);
                            }
                        }
                    }
                }
            }
            catch (Exception e) {
                Log.ErrorFormat("[ThreadID: {0}] {1} {2}", Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), e.VerboseExceptionString());
                throw;
            }
            finally {
                Log.InfoFormat("[ThreadID: {0}] {1} finished.  Time required: {2}.  Sites found: {3}; Servers found in sites: {4}",
                    Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), DateTime.UtcNow.Subtract(startTime).ToString(),
                    adSites.Count, adSites.Values.Sum(x => x.SiteServers.Count));
            }

            return adSites;
        }

        public static List<ComputerProperties> GetDCComputerAccounts(bool useCache = true) {
            Log.InfoFormat("[ThreadID: {0}] {1}", Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName());

            var startTime = DateTime.UtcNow;
            var dcComputerAccounts = new List<ComputerProperties>();

            try {
                if (useCache && DCComputerAccounts.Count > 0) {
                    dcComputerAccounts = DCComputerAccounts;
                }
                else {
                    foreach (var domainInfo in ADDataAccess.ADDomainInfo) {
                        // do not use global cataLog.  Use LocalPartition so that we can get the Operating System and Operating System Version attributes
                        var dcComputerAccountsList = ADDataAccess.GetADObjectProperties<ComputerProperties>(
                            domainInfo.DistinguishedName, null, ADDataAccess.QueryFilters.DomainControllers, PartitionScope.LocalPartition);

                        foreach (var dcComputer in dcComputerAccountsList) {
                            #region Check for server exclusions
                            var excludedServer = ExcludedServers
                                .Where(x =>
                                    (!string.IsNullOrWhiteSpace(x.DN) && Regex.IsMatch(dcComputer.DN, x.DN, RegexOptions.IgnoreCase))
                                    || (!string.IsNullOrWhiteSpace(x.SamAccountName) && Regex.IsMatch(dcComputer.SamAccountName, x.SamAccountName, RegexOptions.IgnoreCase)))
                                .FirstOrDefault();
                            bool serverIsExcluded = false;
                            if (excludedServer != null) {
                                if (excludedServer.ExcludeIfOffline.HasValue && excludedServer.ExcludeIfOffline.Value) {
                                    serverIsExcluded = !Utility.LdapTest(dcComputer.DnsHostName);
                                }
                                else {
                                    serverIsExcluded = true;
                                }
                            }
                            if (serverIsExcluded) {
                                Log.InfoFormat("[ThreadID: {0}] {1} Not testing server: {2} due to the server is on the Excluded Servers list.  Exclusion criteria: {3}",
                                    Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), dcComputer.DN, excludedServer.ToString());
                                continue;
                            }
                            #endregion

                            dcComputerAccounts.Add(dcComputer);
                        }
                    } // foreach (var domainInfo in ADDataAccess.ADDomainInfo)

                    #region Sort list with headquarters domain controllers at the top, to test them first.
                    var hdqServers = dcComputerAccounts
                        .Where(x => x.SamAccountName.StartsWith("HDQ", StringComparison.Ordinal))
                        .ToList();

                    dcComputerAccounts = dcComputerAccounts
                        .Where(x => !x.SamAccountName.StartsWith("HDQ", StringComparison.OrdinalIgnoreCase))
                        .ToList();

                    dcComputerAccounts = hdqServers.Concat(dcComputerAccounts).ToList();
                    #endregion

                    lock (DCComputerAccounts) {
                        DCComputerAccounts.Clear();
                        DCComputerAccounts.AddRange(dcComputerAccounts);
                    }
                }
            }
            catch (Exception e) {
                Log.ErrorFormat("[ThreadID: {0}] {1} {2}", Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), e.VerboseExceptionString());
                throw;
            }
            finally {
                Log.InfoFormat("[ThreadID: {0}] {1} finished.  Time required: {2} DCComputerAccounts: {3}",
                    Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), DateTime.UtcNow.Subtract(startTime).ToString(), dcComputerAccounts.Count);
            }

            return dcComputerAccounts;
        }

        public static Dictionary<string, List<Gpo>> GetDomainGPOs(bool useCache = true) {
            Log.InfoFormat("[ThreadID: {0}] {1}", Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName());

            var startTime = DateTime.UtcNow;
            var domainGpos = new Dictionary<string, List<Gpo>>(StringComparer.OrdinalIgnoreCase);

            try {
                if (useCache && DomainGPOs.Count > 0) {
                    domainGpos = DomainGPOs;
                }
                else {
                    foreach (var domainInfo in ADDataAccess.ADDomainInfo) {
                        if (!domainGpos.ContainsKey(domainInfo.DNSDomainName)) {
                            domainGpos.Add(domainInfo.DNSDomainName, new List<Gpo>());
                        }

                        var gpDomain = new GPDomain(domainInfo.DNSDomainName, DCUsage.UsePrimaryDC);
                        GpoCollection gposForDomain = gpDomain.GetAllGpos();

                        foreach (var gpo in gposForDomain) {
                            domainGpos[domainInfo.DNSDomainName].Add(gpo);
                        }

                        lock (DomainGPOs) {
                            DomainGPOs.Clear();
                            foreach (var kvp in domainGpos) {
                                DomainGPOs.Add(kvp.Key, kvp.Value);
                            }
                        }

                        Log.InfoFormat("[ThreadID: {0}] {1} Domain: {2} Group Policy Objects: {3}",
                            Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), domainInfo.DNSDomainName, domainGpos[domainInfo.DNSDomainName].Count);
                    }
                }
            }
            catch (Exception e) {
                Log.ErrorFormat("[ThreadID: {0}] {1} {2}", Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), e.VerboseExceptionString());
                throw;
            }
            finally {
                Log.InfoFormat("[ThreadID: {0}] {1} finished.  Time required: {2}",
                    Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), DateTime.UtcNow.Subtract(startTime).ToString());
            }

            return domainGpos;
        }

        public static List<ExcludedServer> GetExcludedServers() {
            Log.InfoFormat("[ThreadID: {0}] {1}", Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName());

            var startTime = DateTime.UtcNow;
            string documentPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ExcludedServers.xml");

            Log.InfoFormat("[ThreadID: {0}] {1} Checking for ExcludedServers.xml path: {2}", 
                Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), documentPath);
            var excludedServers = new List<ExcludedServer>();

            if (!File.Exists(documentPath)) {
                Log.InfoFormat("{0} Unable to locate exclusions file: {1}", Utility.CurrentMethodName(), documentPath);
            }
            else {
                try {
                    using (var fileStream = new FileStream(documentPath, FileMode.OpenOrCreate)) {
                        var xmlSerializer = new XmlSerializer(typeof(List<ExcludedServer>));
                        excludedServers = xmlSerializer.Deserialize(fileStream) as List<ExcludedServer>;
                    }

                    excludedServers = excludedServers
                        .Where(x => x.IsValid)
                        .ToList();

                    lock (ExcludedServers) {
                        ExcludedServers.Clear();
                        ExcludedServers.AddRange(excludedServers);
                    }
                }
                catch (Exception e) {
                    Log.InfoFormat("[ThreadID: {0}] {1} error processing exclusions file: {1} Exception: {2}",
                        Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), documentPath, e.VerboseExceptionString());
                    throw;
                }
                finally {
                    Log.InfoFormat("[ThreadID: {0}] {1} finished.  Time required: {2}.  Excluded server entries: {3}",
                        Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), DateTime.UtcNow.Subtract(startTime).ToString(), excludedServers.Count);

                    foreach (var entry in excludedServers) {
                        Log.InfoFormat("[ThreadID: {0}] {1}  Excluded server entry: {2}",
                            Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), entry.ToString());

                    }
                }
            }

            return excludedServers;
        }

        public static Dictionary<string, Dictionary<string, string>> GetFSMORoleOwners() {
            Log.InfoFormat("[ThreadID: {0}] {1}", Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName());

            var startTime = DateTime.UtcNow;
            var fsmoRoleOwners = new Dictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase);

            try {
                foreach (var domainInfo in ADDataAccess.ADDomainInfo) {
                    if (fsmoRoleOwners.ContainsKey(domainInfo.DNSDomainName)) continue;
                    fsmoRoleOwners.Add(domainInfo.DNSDomainName, new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase));

                    fsmoRoleOwners[domainInfo.DNSDomainName].Add("PDCEmulatorOwner", domainInfo.PDCEmulatorOwner);
                    fsmoRoleOwners[domainInfo.DNSDomainName].Add("InfrastractureManagerOwner", domainInfo.InfrastractureManagerOwner);
                    fsmoRoleOwners[domainInfo.DNSDomainName].Add("RIDManagerOwner", domainInfo.RIDManagerOwner);
                }
            }
            catch (Exception e) {
                Log.ErrorFormat("[ThreadID: {0}] {1} {2}", Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), e.VerboseExceptionString());
                throw;
            }
            finally {
                Log.InfoFormat("[ThreadID: {0}] {1} finished.  Time required: {2}",
                    Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), DateTime.UtcNow.Subtract(startTime).ToString());
            }

            return fsmoRoleOwners;
        }

        /// <summary>
        /// Tag objects are typically lightweight Active Directory objects such as a User Contact.
        /// There should be one in each domain.
        /// One attribute of the tag object is used to store a value, the Description field. 
        /// The value is the datetime when it was last udpated, at the end of the previous health check.
        /// The Tag object is updated at the end of each health check.
        /// </summary>
        /// <param name="dcHealthCheckTagRDN">
        /// The relative distinguished name of the tag object. Example: CN=DCHealthCheck,CN=Users
        /// </param>
        /// <remarks>Tag Objects are created here if they do not exist.</remarks>
        public static List<TagObject> GetTagObjects(string dcHealthCheckTagRDN) {
            Log.InfoFormat("[ThreadID: {0}] {1}", Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName());

            var startTime = DateTime.UtcNow;
            List<TagObject> tagObjects = null;

            try {
                using (var entityManager = new EntityManager()) {
                    tagObjects = entityManager.Set<TagObject>().AsNoTracking().ToList();
                    if (tagObjects == null) {
                        tagObjects = new List<TagObject>();
                    }

                    foreach (var domainInfo in ADDataAccess.ADDomainInfo) {
                        string healthCheckTagDN = string.Format("{0},{1}", dcHealthCheckTagRDN, domainInfo.DistinguishedName);

                        var tagObject = tagObjects
                            .Where(x =>
                                string.Equals(x.DNSDomain, domainInfo.DNSDomainName, StringComparison.OrdinalIgnoreCase) &&
                                string.Equals(x.DN, healthCheckTagDN, StringComparison.OrdinalIgnoreCase))
                            .FirstOrDefault();

                        if (tagObject == null) {
                            tagObject = new TagObject() { DNSDomain = domainInfo.DNSDomainName, DN = healthCheckTagDN, DetachedState = DetachedState.Added };
                            tagObjects.Add(tagObject);
                        }
                    } // foreach (var domainInfo in ADDataAccess.ADDomainInfo)


                    tagObjects = tagObjects
                        .Where(x => ADDataAccess.ADDomainInfo.Any(y => y.DNSDomainName == x.DNSDomain))
                        .ToList();
                }
            }
            catch (Exception e) {
                Log.InfoFormat("[ThreadID: {0}] {1} Exception: {2}",
                    Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), e.VerboseExceptionString());
                throw;
            }
            finally {
                Log.InfoFormat("[ThreadID: {0}] {1} finished.  Time required: {2}",
                    Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), DateTime.UtcNow.Subtract(startTime).ToString());
            }

            return tagObjects;
        }

        /// <summary>
        /// Find the PDC Emulator for each domain, get the list of directories from \\server\sysvol\domain\Policies,
        /// and use the most recent folder as the target folder path for the Sysvol test
        /// </summary>
        public static Dictionary<string, GroupPolicySysvolInfo> GetPDCSysvolTestFolders() {
            Log.InfoFormat("[ThreadID: {0}] {1}", Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName());

            var startTime = DateTime.UtcNow;
            var pdcEmulatorFSMOOwners = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
            var domainPDCSysvolPolicyFolders = new Dictionary<string, GroupPolicySysvolInfo>(StringComparer.OrdinalIgnoreCase);

            if (DCComputerAccounts.Count == 0) GetDCComputerAccounts();
            if (DomainGPOs.Count == 0) GetDomainGPOs();

            foreach (var domainInfo in ADDataAccess.ADDomainInfo) {
                pdcEmulatorFSMOOwners.Add(domainInfo.DNSDomainName, domainInfo.PDCEmulatorOwner);
            }

            try {

                foreach (var pdcEmulatorFSMOOwnerInfo in pdcEmulatorFSMOOwners) {
                    string dnsDomainName = pdcEmulatorFSMOOwnerInfo.Key;

                    var dcComputerAccount = DCComputerAccounts
                        .Where(x => pdcEmulatorFSMOOwnerInfo.Value.ToLower().Contains(x.SamAccountNameFriendly.ToLower()))
                        .FirstOrDefault();

                    if (dcComputerAccount == null) {
                        continue;
                    }

                    string sysvolPath = string.Format(@"\\{0}\sysvol", dcComputerAccount.DnsHostName);
                    string sysvolPolicyFoldersPath = string.Format(@"{0}\{1}\Policies", sysvolPath, dcComputerAccount.DNSDomain);

                    if (!Directory.Exists(sysvolPolicyFoldersPath)) continue;

                    var groupPolicyFolderList = new List<GroupPolicySysvolInfo>();

                    foreach (var serverSysvoGpolDirectoryPath in Directory.GetDirectories(sysvolPolicyFoldersPath, "{*}", SearchOption.TopDirectoryOnly)) {
                        try {
                            Gpo gpo = DomainGPOs[dnsDomainName]
                                .Where(x => serverSysvoGpolDirectoryPath.ToLowerInvariant().Contains(x.Id.ToString()))
                                .FirstOrDefault();

                            if (gpo != null) {
                                var groupPolicySysvolInfo = new GroupPolicySysvolInfo();
                                var groupPolicySysvolInfoBuilder = new GroupPolicySysvolInfoBuilder(groupPolicySysvolInfo, gpo.DisplayName, serverSysvoGpolDirectoryPath);
                                groupPolicySysvolInfo = groupPolicySysvolInfoBuilder.GetGroupPolicySysvolInfo();
                                groupPolicyFolderList.Add(groupPolicySysvolInfo);
                            }
                        }
                        catch (Exception e) {
                            Log.WarnFormat("[ThreadID: {0}] {1} DC: {2} Error: {3}",
                                Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), dcComputerAccount.DnsHostName, e.VerboseExceptionString());
                        }
                    }

                    if (groupPolicyFolderList.Count > 0) {
                        groupPolicyFolderList = groupPolicyFolderList.OrderByDescending(x => x.ModifiedUTC).ToList();

                        foreach (var groupPolicyFolderInfo in groupPolicyFolderList) {
                            // do not use if too recent - it may not have replicated yet.
                            var groupPolicyDirectoryMinimumAge = new TimeSpan(2, 0, 0, 0);
                            if (groupPolicyFolderInfo.ModifiedUTC > DateTime.UtcNow.Add(-groupPolicyDirectoryMinimumAge)) continue;

                            Log.InfoFormat("[ThreadID: {0}] {1} Domain: {2}.  Sysvol policy folder: {3} ModifiedUTC: {4}",
                                Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), pdcEmulatorFSMOOwnerInfo.Key,
                                groupPolicyFolderInfo.DirectoryName, groupPolicyFolderInfo.ModifiedUTC.ToString("yyyy-MMM-dd HH:mm"));

                            domainPDCSysvolPolicyFolders.Add(dcComputerAccount.DNSDomain, groupPolicyFolderInfo);
                            break;
                        }
                    }
                    else {
                        Log.WarnFormat("[ThreadID: {0}] {1} Zero sysvol group policy folders found for server: {2}",
                            Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), dcComputerAccount.DnsHostName);
                    }
                } // foreach (var pdcEmulatorFSMOOwnerInfo in pdcEmulatorFSMOOwners)
            }
            catch (Exception e) {
                Log.ErrorFormat("[ThreadID: {0}] {1} {2}", Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), e.VerboseExceptionString());
                throw;
            }
            finally {
                Log.InfoFormat("[ThreadID: {0}] {1} finished.  Time required: {2}.  Sysvol policy folders found for domains: {3}",
                    Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), DateTime.UtcNow.Subtract(startTime).ToString(), domainPDCSysvolPolicyFolders.Count);
            }

            return domainPDCSysvolPolicyFolders;
        }

        private static void STPPostExecute(IWorkItemResult wir) {
            Log.DebugFormat("[ThreadID: {0}] {1} STP Active Threads: {2} Waiting Callbacks: {3}",
                Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), STP.ActiveThreads, STP.WaitingCallbacks);

            if (wir.Exception != null) {
                Exception exception = wir.Exception as Exception;

                if (exception != null) {
                    Log.WarnFormat("[ThreadID: {0}] {1} STP Exception: {2}",
                        Thread.CurrentThread.ManagedThreadId, Utility.CurrentMethodName(), exception.VerboseExceptionString());
                }
            }
        } 
        #endregion
    }
}
